package ckw.election;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import javax.inject.Inject;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;

import ckw.communication.CommunicationService;
import ckw.database.DatabaseService;
import ckw.database.model.AdministratorToken;
import ckw.database.model.Election;
import ckw.database.model.ElectionChanges;
import ckw.database.model.ElectionNode;
import ckw.database.model.ElectionNonResidentVoter;
import ckw.database.model.ElectionResidentVoter;
import ckw.database.model.Member;
import ckw.database.model.Node;
import ckw.database.model.Vote;
import ckw.database.model.Voter;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;

import core.cards.election.model.ElectionBackend;
import core.cards.election.model.ElectionResultBackend;
import core.communication.message.ElectionConfigurationMessage;
import core.communication.message.Message;
import core.communication.message.VotersPackageMessage;
import core.communication.message.VotesPackageMessage;
import core.database.ElectionStatus;
import core.exceptions.ObjectDeleteException;
import core.exceptions.WrongActionException;
import core.utils.Converter;

@Service
public class ElectionServiceImpl implements ElectionService, ApplicationContextAware {
	private static final Logger L = LoggerFactory.getLogger(ElectionServiceImpl.class);

	ApplicationContext context;

	private DatabaseService databaseService;

	@Inject
	public ElectionServiceImpl(DatabaseService databaseService) {
		L.debug("Tworze ElectionService");

		Assert.notNull(databaseService);

		this.databaseService = databaseService;

		handleUnparsedVotes();

		L.debug("Utworzono ElectionService");
	}

	@Override
	public void addMember(ElectionNode electionNode, Member member) {
		AdministratorToken token;
		if (electionNode.getId() != null && member.getId() != null) {
			token = databaseService.findAdministratorTokenByMemberAndElectionNode(member, electionNode);
			if (token != null) {
				if (!electionNode.getAdministratorTokens().contains(token)) {
					electionNode.getAdministratorTokens().add(token);
					member.getTokens().add(token);
				}
				return;
			}
		}
		token = new AdministratorToken(member, electionNode);
	}

	@Override
	public void removeMember(ElectionNode electionNode, Member member) {
		AdministratorToken token;
		if (electionNode.getId() != null && member.getId() != null)
			token = databaseService.findAdministratorTokenByMemberAndElectionNode(member, electionNode);
		else
			token = null;

		if (token != null) {
			member.getTokens().remove(token);
			electionNode.getAdministratorTokens().remove(token);
		} else {
			List<AdministratorToken> commonTokens = Lists.newArrayList(electionNode.getAdministratorTokens());
			commonTokens.retainAll(member.getTokens());

			member.getTokens().removeAll(commonTokens);
			electionNode.getAdministratorTokens().removeAll(commonTokens);
		}
	}

	@Override
	public UUID generateToken(AdministratorToken token) {
		UUID value = UUID.randomUUID();
		token.setValue(value.toString());

		token = databaseService.storeAdministratorToken(token);

		ElectionChanges changes = databaseService.getElectionChanges(token.getElectionNode().getNode().getLocatorURI(), token.getElectionNode().getElection().getIdentificationField());

		if (changes == null) {
			changes = new ElectionChanges(token.getElectionNode().getNode().getLocatorURI(), token.getElectionNode().getElection().getIdentificationField());

			changes.addAddMemberAction(token.getValue(), token.getMember());

			changes = databaseService.storeElectionChanges(changes);

			Message message = new ElectionConfigurationMessage(changes.getUri(), changes.getElectionIdentificationField(), changes.getElectionActions());
			context.getBean(CommunicationService.class).send(message);
			databaseService.deleteElectionChanges(changes);
		} else {
			changes.addAddMemberAction(token.getValue(), token.getMember());

			changes = databaseService.storeElectionChanges(changes);
		}

		return value;
	}

	@Override
	public void addResidentVoter(Election election, Voter voter) {
		if (voter != null) {
			// Usuwamy z Election
			List<ElectionNonResidentVoter> nrVoters = Lists.newArrayList(election.getVoters());
			nrVoters.retainAll(voter.getNonResidentElections());

			if (nrVoters.size() == 1)
				nrVoters.get(0).removeFromElections();

			List<ElectionResidentVoter> rVoters;
			for (ElectionNode eNode : election.getNodes()) {
				rVoters = Lists.newArrayList(eNode.getVoters());
				rVoters.retainAll(voter.getElections());

				if (rVoters.size() == 1)
					return;
			}

			ElectionNode electionNode = null;
			List<ElectionNode> commonElectionNodes = Lists.newArrayList(election.getNodes());

			for (ElectionNode e : commonElectionNodes) {
				if (electionNode == null)
					electionNode = (e.getNode().getId().equals(voter.getNode().getId())) ? e : null;
				else
					break;
			}

			if (electionNode != null) {
				if (electionNode.getId() != null) {
					ElectionResidentVoter rVoter = databaseService.findResidentVoterByVoterAndElectionNode(voter, commonElectionNodes.get(0));
					if (rVoter != null) {
						rVoter.addToElections(voter, commonElectionNodes.get(0));
						return;
					}
				}
				new ElectionResidentVoter(voter, electionNode);
			}
		}
	}

	@Override
	public void removeResidentVoter(ElectionResidentVoter electionResidentVoter) {
		electionResidentVoter.removeFromElections();
	}

	@Override
	public void addNonResidentVoter(Election election, Voter voter) {
		if (voter != null) {
			// Usunięcie votera z ElectionNode
			List<ElectionResidentVoter> rVoters;
			for (ElectionNode eNode : election.getNodes()) {
				rVoters = Lists.newArrayList(eNode.getVoters());
				rVoters.retainAll(voter.getElections());

				if (rVoters.size() == 1) {
					rVoters.get(0).removeFromElections();
					break;
				}
			}

			// Sprawdzamy czy już jest dodany jako nonResident
			List<ElectionNonResidentVoter> nrVoters = Lists.newArrayList(election.getVoters());
			nrVoters.retainAll(voter.getNonResidentElections());

			if (nrVoters.isEmpty()) {
				if (election.getId() != null) {
					ElectionNonResidentVoter nrVoter = databaseService.findNonResidentVoterByVoterAndElection(voter, election);
					if (nrVoter != null) {
						nrVoter.addToElections(voter, election);
						return;
					}
				}
				new ElectionNonResidentVoter(voter, election);
			}
		}
	}

	@Override
	public void removeNonResidentVoter(ElectionNonResidentVoter electionNonResidentVoter) {
		electionNonResidentVoter.removeFromElections();
	}

	@Override
	public Election saveElection(Election election) {
		initElectionResults(election);
		election = databaseService.storeElection(election);
		prepareNewElectionChangeMessages(election);
		return election;
	}

	@Override
	public Election updateElection(Election election) throws WrongActionException {
		if (election.getStatus() == ElectionStatus.COMPLETED || election.getStatus() == ElectionStatus.INPROGRESS)
			throw new WrongActionException("Nie można edytować wyborów już rozpoczętych/zakończonych");

		Election oldElection = databaseService.getElection(election.getId());
		oldElection.getVoters().size();
		for (ElectionNode node : oldElection.getNodes())
			node.getVoters().size();

		election.setStatus(ElectionStatus.UNSEND);
		initElectionResults(election);
		election = databaseService.storeElection(election);
		prepareElectionChangeMessages(election, oldElection);
		return election;
	}

	@Override
	public void deleteElection(Election election) throws ObjectDeleteException {
		databaseService.deleteElection(election);

		for (ElectionNode node : election.getNodes()) {
			ElectionChanges changes = databaseService.getElectionChanges(node.getNode().getLocatorURI(), election.getIdentificationField());

			if (changes == null)
				changes = new ElectionChanges(node.getNode().getLocatorURI(), election.getIdentificationField());
			else
				changes.getElectionActions().clear();

			changes.addRemoveElectionAction();

			changes = databaseService.storeElectionChanges(changes);

			Message message = new ElectionConfigurationMessage(changes.getUri(), changes.getElectionIdentificationField(), changes.getElectionActions());
			context.getBean(CommunicationService.class).send(message);
			databaseService.deleteElectionChanges(changes);
		}
	}

	private void prepareNewElectionChangeMessages(Election election) {
		for (ElectionNode node : election.getNodes()) {
			ElectionChanges changes = new ElectionChanges(node.getNode().getLocatorURI(), election.getIdentificationField());

			// Dodajemy election details
			changes.addAddEditElectionDetailsAction(election);

			// Dodajemy Memberów
			for (AdministratorToken token : node.getAdministratorTokens())
				changes.addAddMemberAction(token.getValue(), token.getMember());

			// Dodajemy ResidentVoterów
			for (ElectionResidentVoter voter : node.getVoters())
				changes.addAddResidentVoterAction(voter.getVoter());

			// Dodajemy NonResidentVoterów
			for (ElectionNonResidentVoter voter : election.getVoters())
				changes.addAddNonResidentVoterAction(voter.getVoter());

			databaseService.storeElectionChanges(changes);
		}
	}

	private void prepareElectionChangeMessages(Election election, Election oldElection) {
		Map<Long, ElectionNode> oldNodes = Maps.newHashMap();
		for (ElectionNode node : oldElection.getNodes())
			oldNodes.put(node.getId(), node);

		Map<Long, ElectionNode> nodes = Maps.newHashMap();
		for (ElectionNode node : election.getNodes())
			nodes.put(node.getId(), node);

		Map<Long, ElectionNonResidentVoter> oldNonResidentVoters = Maps.newHashMap();
		for (ElectionNonResidentVoter voter : oldElection.getVoters())
			oldNonResidentVoters.put(voter.getId(), voter);

		Map<Long, ElectionNonResidentVoter> nonResidentVoters = Maps.newHashMap();
		for (ElectionNonResidentVoter voter : election.getVoters())
			nonResidentVoters.put(voter.getId(), voter);

		// Usuwamy wybory w usuniętych węzłach
		for (ElectionNode node : oldElection.getNodes()) {
			if (!nodes.containsKey(node.getId())) {
				ElectionChanges changes = databaseService.getElectionChanges(node.getNode().getLocatorURI(), election.getIdentificationField());
				if (changes == null)
					changes = new ElectionChanges(node.getNode().getLocatorURI(), election.getIdentificationField());
				else
					changes.getElectionActions().clear();

				changes.addRemoveElectionAction();

				databaseService.storeElectionChanges(changes);
			}
		}

		for (ElectionNode node : election.getNodes()) {
			// Dodajemy wybory w nowych węzłach
			if (!oldNodes.containsKey(node.getId())) {
				ElectionChanges changes = new ElectionChanges(node.getNode().getLocatorURI(), election.getIdentificationField());

				// Dodajemy election details
				changes.addAddEditElectionDetailsAction(election);

				// Dodajemy Memberów
				for (AdministratorToken token : node.getAdministratorTokens())
					changes.addAddMemberAction(token.getValue(), token.getMember());

				// Dodajemy ResidentVoterów
				for (ElectionResidentVoter voter : node.getVoters())
					changes.addAddResidentVoterAction(voter.getVoter());

				// Dodajemy NonResidentVoterów
				for (ElectionNonResidentVoter voter : election.getVoters())
					changes.addAddNonResidentVoterAction(voter.getVoter());

				databaseService.storeElectionChanges(changes);
			} else { // Uaktualniamy wybory w już wcześniej wybranych węzłach
				ElectionChanges changes = databaseService.getElectionChanges(node.getNode().getLocatorURI(), election.getIdentificationField());

				if (changes == null)
					changes = new ElectionChanges(node.getNode().getLocatorURI(), election.getIdentificationField());

				ElectionNode oldNode = oldNodes.get(node.getId());

				// Uaktualniamy election details
				changes.addAddEditElectionDetailsAction(election);

				// Uaktualniamy Memberów
				Map<Long, AdministratorToken> oldTokens = Maps.newHashMap();
				for (AdministratorToken token : oldNode.getAdministratorTokens())
					oldTokens.put(token.getId(), token);

				Map<Long, AdministratorToken> tokens = Maps.newHashMap();
				for (AdministratorToken token : node.getAdministratorTokens())
					tokens.put(token.getId(), token);

				for (AdministratorToken token : oldNode.getAdministratorTokens())
					if (!tokens.containsKey(token.getId()))
						changes.addRemoveMemberAction(token.getMember().getIdentificationField());

				for (AdministratorToken token : node.getAdministratorTokens())
					if (!oldTokens.containsKey(token.getId()))
						changes.addAddMemberAction(token.getValue(), token.getMember());

				// Uaktualniamy ResidentVoterów
				Map<Long, ElectionResidentVoter> oldResidentVoters = Maps.newHashMap();
				for (ElectionResidentVoter voter : oldNode.getVoters())
					oldResidentVoters.put(voter.getId(), voter);

				Map<Long, ElectionResidentVoter> residentVoters = Maps.newHashMap();
				for (ElectionResidentVoter voter : node.getVoters())
					residentVoters.put(voter.getId(), voter);

				for (ElectionResidentVoter voter : oldNode.getVoters())
					if (!residentVoters.containsKey(voter.getId()))
						changes.addRemoveResidentVoterAction(voter.getVoter().getIdentificationField());

				for (ElectionResidentVoter voter : node.getVoters())
					if (!oldResidentVoters.containsKey(voter.getId()))
						changes.addAddResidentVoterAction(voter.getVoter());

				// Uaktualniamy NonResidentVoterów
				for (ElectionNonResidentVoter voter : oldElection.getVoters())
					if (!nonResidentVoters.containsKey(voter.getId()))
						changes.addRemoveNonResidentVoterAction(voter.getVoter().getIdentificationField());

				for (ElectionNonResidentVoter voter : election.getVoters())
					if (!oldNonResidentVoters.containsKey(voter.getId()))
						changes.addAddNonResidentVoterAction(voter.getVoter());

				databaseService.storeElectionChanges(changes);
			}
		}
	}

	@Override
	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
		this.context = applicationContext;
	}

	private void handleUnparsedVotes() {
		List<Vote> unparsedVotes = databaseService.getUnparsedVotes();

		Map<Long, List<Vote>> votesMap = new HashMap<Long, List<Vote>>();

		for (Vote vote : unparsedVotes) {
			Long electionIdentificationField = vote.getElection().getIdentificationField();
			if (!votesMap.containsKey(electionIdentificationField))
				votesMap.put(electionIdentificationField, new ArrayList<Vote>());
			votesMap.get(electionIdentificationField).add(vote);
		}

		for (Long electionIdentificationField : votesMap.keySet())
			recalculateElectionResults(electionIdentificationField, votesMap.get(electionIdentificationField));
	}

	@Override
	public void handleVotesPackage(VotesPackageMessage message) {
		List<VotesPackageMessage> packages = new ArrayList<VotesPackageMessage>();
		packages.add(message);

		while (!packages.isEmpty()) {
			VotesPackageMessage p = packages.get(0);
			packages.remove(0);
			if (p.getPackages().isEmpty()) {
				List<Vote> votes = new ArrayList<Vote>();
				for (String v : p.getVotes())
					votes.add(new Vote(v, p.getNodeLocatorUri()));
				votes = databaseService.saveVotes(p.getElectionIdentificationField(), votes);
				recalculateElectionResults(p.getElectionIdentificationField(), votes);
			} else {
				packages.addAll(p.getPackages());
			}
		}
	}

	@Async
	@Transactional
	private void recalculateElectionResults(Long electionIdentificationField, List<Vote> votes) {
		Election election = databaseService.getElectionByIdentificationField(electionIdentificationField);
		if (election != null) {
			String r = election.getResults();
			if (r == null) {
				initElectionResults(election);
				r = election.getResults();
			}
			ElectionResultBackend results = (ElectionResultBackend) Converter.xmlToObject(r);
			for (Vote vote : votes) {
				results.addVote((ElectionBackend) Converter.xmlToObject(vote.getCard()), vote.getRkwLocatorURI());
				vote.setParsed(true);
			}

			election.setResults(Converter.objectToXML(results));
			databaseService.storeElection(election);
			databaseService.updateVotes(votes);
		}
	}

	@Override
	public void initElectionResults(Election election) {
		ElectionResultBackend results = new ElectionResultBackend((ElectionBackend) Converter.xmlToObject(election.getCards()));
		int maxVotersCount = election.getVoters().size();

		for (ElectionNode node : election.getNodes()) {
			Node n = node.getNode();
			int votersCount = node.getVoters().size();
			results.addRkw(n.getLocatorURI(), n.getName(), votersCount);
			maxVotersCount += votersCount;
		}
		results.setMaxVotersCount(maxVotersCount);

		election.setResults(Converter.objectToXML(results));
	}

	@Override
	@Transactional
	public void handleVotersPackage(VotersPackageMessage message) {
		Election election = databaseService.getElectionByIdentificationField(message.getElectionIdentificationField());
		ElectionResultBackend results = (ElectionResultBackend) Converter.xmlToObject(election.getResults());

		List<String> votersIds = message.getVotersIds();

		for (String vId : votersIds) {
			ElectionResidentVoter voter = databaseService.getResidentVoterByVoterIdentificationFieldAndElectionIdentificationField(vId, message.getElectionIdentificationField());
			if (voter != null) {
				voter.setHasVoted(true);
				databaseService.updateElectionResidentVoter(voter);
				results.increaseVotersCount(1, voter.getElectionNode().getNode().getLocatorURI());
			}
		}
		election.setResults(Converter.objectToXML(results));
		databaseService.storeElection(election);
	}

	@Override
	public void addNonresidentVoterToElectionResults(Long electionIdentificationField) {
		Election election = databaseService.getElectionByIdentificationField(electionIdentificationField);
		if (election != null) {
			ElectionResultBackend results = (ElectionResultBackend) Converter.xmlToObject(election.getResults());
			results.increaseVotersCount(1, null);
		}
	}
}
